"""
认证视图模块
处理用户登录、钉钉认证回调、用户信息获取等功能
"""

# 导入FastAPI相关模块
# APIRouter: 用于创建路由实例，组织API端点
# HTTPException: 用于抛出HTTP异常，向客户端返回错误信息
# Depends: FastAPI的依赖注入系统，用于注入依赖项如数据库会话
# status: HTTP状态码常量，如200、400、401等
from fastapi import APIRouter, HTTPException, Depends, status

# HTTPBearer: HTTP Bearer认证方案，用于处理Bearer Token认证
# HTTPAuthorizationCredentials: 认证凭据数据模型，用于获取认证信息
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

# SQLAlchemy数据库会话，用于数据库操作
from sqlalchemy.orm import Session

# Pydantic数据模型基类，用于数据验证和序列化
from pydantic import BaseModel

# 可选类型提示，用于标记可选参数
from typing import Optional

# 异步HTTP客户端库，用于发起网络请求
import httpx

# 导入项目内部模块
# get_db: 数据库会话依赖函数，通过依赖注入获取数据库会话
from database import get_db

# User: 用户数据模型，对应数据库中的users表
from models.user import User

# create_access_token: 创建JWT访问令牌的工具函数
# verify_token: 验证JWT令牌有效性的工具函数
from tools.auth import create_access_token, verify_token

# generate_dingtalk_auth_url: 生成钉钉授权URL的工具函数
# dingtalk_client: 钉钉客户端实例，用于调用钉钉API
from tools.dingtalk import generate_dingtalk_auth_url, dingtalk_client

# settings: 应用配置，包含各种配置项如密钥、URL等
from config import settings

# 创建API路由实例，用于注册端点
# 所有以"/api"为前缀的认证相关端点都会注册到这个路由实例上
router = APIRouter()

# 创建HTTP Bearer认证实例，用于保护需要认证的端点
# 当端点需要认证时，会自动验证请求头中的Authorization字段
security = HTTPBearer()


class DingtalkCallbackRequest(BaseModel):
    """钉钉回调请求数据模型 - 定义了钉钉回调接口需要的数据结构"""
    # 必需的授权码字段，钉钉授权成功后会返回这个code
    # 客户端需要将这个code发送给后端，后端用它换取用户信息
    authCode: str
    
    # 可选的状态字段，用于防止CSRF攻击
    # 前端请求授权URL时会生成一个state，钉钉回调时会原样返回
    # 后端可以验证这个state是否匹配来防止CSRF攻击
    state: Optional[str] = None


class DingtalkAuthResponse(BaseModel):
    """钉钉授权URL响应数据模型 - 定义了获取钉钉授权URL接口的响应结构"""
    # 授权URL，前端需要跳转到这个URL进行钉钉授权
    auth_url: str
    
    # 状态码，用于防止CSRF攻击，需要在回调时验证
    state: str


class LoginResponse(BaseModel):
    """登录响应数据模型 - 定义了登录接口的响应结构"""
    # JWT访问令牌，客户端后续请求需要在Authorization头中携带这个token
    access_token: str
    
    # 令牌类型，默认为"bearer"，表示这是一个Bearer Token
    token_type: str = "bearer"
    
    # 用户信息字典，包含用户的基本信息
    user: dict


class UserInfoResponse(BaseModel):
    """用户信息响应数据模型 - 定义了获取用户信息接口的响应结构"""
    # 用户ID，用户的唯一标识符
    id: int
    
    # 用户名（可选），用户的登录名
    username: Optional[str]
    
    # 昵称（可选），用户设置的昵称
    nickname: Optional[str]
    
    # 手机号（可选），用户的手机号码
    mobile: Optional[str]
    
    # 邮箱（可选），用户的邮箱地址
    email: Optional[str]
    
    # 头像URL（可选），用户头像的图片地址
    avatar: Optional[str]
    
    # 钉钉ID（可选），用户在钉钉系统中的唯一标识
    dingtalk_id: Optional[str]
    
    # 创建时间，用户账号的创建时间
    created_at: str


@router.get("/dingtalk/auth-url", response_model=DingtalkAuthResponse)
async def get_dingtalk_auth_url():
    """
    获取钉钉授权URL端点
    客户端通过调用此接口获取钉钉登录的授权URL
    
    业务流程：
    1. 用户在前端点击钉钉登录按钮
    2. 前端调用此接口获取授权URL
    3. 前端跳转到授权URL，用户在钉钉页面确认授权
    4. 钉钉回调到前端指定的回调页面
    5. 前端从回调URL中提取authCode，发送给后端
    """
    # 调用工具函数生成钉钉授权URL和状态码
    # generate_dingtalk_auth_url函数会生成一个包含client_id、redirect_uri等参数的URL
    # 同时生成一个随机的state用于防止CSRF攻击
    auth_url, state = generate_dingtalk_auth_url()
    
    # 返回授权URL和状态码，符合DingtalkAuthResponse模型
    # 前端收到这个响应后，会跳转到auth_url进行钉钉授权
    return {
        "auth_url": auth_url,
        "state": state
    }


@router.post("/dingtalk/callback", response_model=LoginResponse)
async def dingtalk_callback(
    request: DingtalkCallbackRequest,  # 请求体数据，包含authCode和state
    db: Session = Depends(get_db)      # 数据库会话依赖注入，用于数据库操作
):
    """
    处理钉钉授权回调端点（使用新版钉钉API）
    钉钉授权完成后会回调此接口，完成用户登录流程
    
    核心业务逻辑：
    1. 使用authCode换取用户信息（新版钉钉API一步到位）
    2. 检查用户是否已存在系统中
    3. 如果存在则更新用户信息，否则创建新用户
    4. 生成JWT token并返回给前端
    """
    # 使用新版API通过code直接获取用户信息
    # 调用钉钉客户端的get_user_info_by_code方法，使用授权码获取用户信息
    # 这是新版钉钉API的优势，一步就能获取用户信息，不需要先换access_token再获取用户信息
    user_info = await dingtalk_client.get_user_info_by_code(request.authCode)
    
    # 如果未获取到用户信息，说明授权码无效或者网络异常
    # 抛出400错误，告诉客户端授权失败
    if not user_info:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="获取钉钉用户信息失败"
        )
    
    # 从用户信息中提取关键字段
    # openId是钉钉用户的唯一标识，每个应用中用户都有唯一的openId
    openid = user_info.get("openId")
    
    # unionId是用户在钉钉开放平台的唯一标识，同一个用户在不同应用中的unionId是相同的
    unionid = user_info.get("unionId")
    
    # 如果没有获取到openId，说明返回的用户信息不完整
    # 抛出400错误，告诉客户端获取用户信息失败
    if not openid:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="未获取到用户openId"
        )
    
    # 检查用户是否已存在（通过钉钉ID查询）
    # 在数据库中查找是否已有该钉钉ID的用户
    # 这样可以实现自动登录，已注册用户直接登录，未注册用户自动注册
    user = db.query(User).filter(User.dingtalk_id == openid).first()
    
    # 如果用户已存在，则更新用户信息
    # 这样可以保证用户信息是最新的，比如用户在钉钉中修改了昵称或头像
    if user:
        # 更新昵称（如果用户信息中有提供）
        # 使用get方法，如果钉钉没有返回nickname，则保持原值不变
        user.nickname = user_info.get("nickname", user.nickname)
        
        # 更新手机号（如果用户信息中有提供）
        user.mobile = user_info.get("mobile", user.mobile)
        
        # 更新邮箱（如果用户信息中有提供）
        user.email = user_info.get("email", user.email)
        
        # 更新头像URL（如果用户信息中有提供）
        user.avatar = user_info.get("avatar_url", user.avatar)
        
        # 更新unionId
        user.unionid = unionid
        
        # 设置用户为活跃状态
        # 确保被禁用的用户在重新登录后恢复活跃状态
        user.is_active = True
    else:
        # 如果用户不存在，则创建新用户
        # 这实现了自动注册功能，用户首次使用钉钉登录时自动创建账号
        user = User(
            # 设置钉钉ID，作为用户在系统中的唯一标识
            dingtalk_id=openid,
            
            # 设置unionId，用于跨应用识别同一用户
            unionid=unionid,
            
            # 设置昵称，使用钉钉中的昵称
            nickname=user_info.get("nickname"),
            
            # 设置手机号，使用钉钉中的手机号
            mobile=user_info.get("mobile"),
            
            # 设置邮箱，使用钉钉中的邮箱
            email=user_info.get("email"),
            
            # 设置头像URL，使用钉钉中的头像
            avatar=user_info.get("avatar_url"),
            
            # 自动生成用户名（使用openId前8位）
            # 由于钉钉没有用户名概念，我们自动生成一个唯一用户名
            username=f"user_{openid[:8]}"
        )
        
        # 将新用户添加到数据库会话
        # 此时还没有真正写入数据库，需要commit才会真正保存
        db.add(user)
    
    # 提交数据库事务，将更改保存到数据库
    # 这一步非常重要，没有commit的话，数据不会真正保存到数据库
    db.commit()
    
    # 刷新用户对象，确保获取最新数据
    # commit后数据库中的数据可能有变化，refresh确保我们获取的是最新的数据
    db.refresh(user)
    
    # 生成JWT token数据，包含用户ID和钉钉ID
    # JWT token中包含用户的基本信息，后续请求可以通过解析token获取用户信息
    token_data = {"sub": str(user.id), "dingtalk_id": openid}
    
    # 调用create_access_token生成JWT访问令牌
    # JWT token有过期时间，过期后需要重新登录或使用refresh_token
    jwt_token = create_access_token(token_data)
    
    # 构造并返回登录响应，包含token、token类型和用户信息
    # 前端收到这个响应后，会将token保存起来，后续请求都需要携带这个token
    return {
        "access_token": jwt_token,
        "token_type": "bearer",
        "user": {
            "id": user.id,
            "username": user.username,
            "nickname": user.nickname,
            "mobile": user.mobile,
            "email": user.email,
            "avatar": user.avatar,
            "dingtalk_id": user.dingtalk_id
        }
    }


@router.get("/user/info", response_model=UserInfoResponse)
async def get_user_info(
    # 通过依赖注入获取HTTP认证凭据
    # security是一个HTTPBearer实例，会自动从请求头中提取Authorization字段
    credentials: HTTPAuthorizationCredentials = Depends(security),
    
    # 通过依赖注入获取数据库会话
    # get_db是一个依赖函数，会自动创建数据库会话并在请求结束时关闭
    db: Session = Depends(get_db)
):
    """
    获取当前登录用户信息端点
    客户端通过携带JWT token调用此接口获取当前用户信息
    
    安全机制：
    1. 通过HTTPBearer自动验证Authorization头中的Bearer token
    2. 调用verify_token验证JWT token的有效性和完整性
    3. 从token中提取用户ID，查询数据库获取用户信息
    """
    # 从认证凭据中提取token
    # credentials.credentials就是Authorization头中Bearer后面的token字符串
    token = credentials.credentials
    
    # 验证token有效性
    # verify_token会验证token的签名、过期时间等，如果有效返回payload，否则返回None
    payload = verify_token(token)
    
    # 如果token无效，抛出401未授权错误
    # 告诉客户端token无效，需要重新登录
    if not payload:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="无效的token"
        )
    
    # 从token中提取用户ID
    # payload是JWT token解码后的内容，sub字段是我们存入的用户ID
    user_id = int(payload.get("sub"))
    
    # 查询数据库获取用户信息
    # 使用用户ID在数据库中查找用户信息
    user = db.query(User).filter(User.id == user_id).first()
    
    # 如果用户不存在，抛出404错误
    # 理论上不应该出现这种情况，因为token中的用户ID是有效的
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="用户不存在"
        )
    
    # 构造并返回用户信息响应，符合UserInfoResponse模型
    # 将数据库中的用户信息转换为接口响应格式
    return {
        "id": user.id,
        "username": user.username,
        "nickname": user.nickname,
        "mobile": user.mobile,
        "email": user.email,
        "avatar": user.avatar,
        "dingtalk_id": user.dingtalk_id,
        # 将创建时间转换为ISO格式字符串，便于前端处理
        "created_at": user.created_at.isoformat() if user.created_at else ""
    }